/*
 * ClientClasspathUpdater.java
 *
 * $Id: ClientClasspathUpdater.java,v 1.3 2009-09-17 12:10:05 mario Exp $
 */
package org.ceteca.explica.client.installer;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.zip.ZipFile;

import org.ceteca.explica.client.ClientContext;
import org.ceteca.explica.core.InterfaceErrors;
import org.ceteca.explica.core.InterfaceExplica;
import org.ceteca.explica.core.installer.ResourceException;

/**
* This class extends the local classpath of the Java virtual machine
* to hold the new or updated installed modules. It makes the new modules
* available to the Java virtual machine.
* <br/>
* Responsibilities:
* <ul>
*  <li>Classpath updating for join the new available modules.</li>
* </ul>
* <br/>
* @author Mario García García <mario@imagos.es>
 * Copyright (c) 2008 Fundación Centro Tecnolóxico da Carne
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <br/>
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * <br/>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
public class ClientClasspathUpdater {
	private static final Class[] parameters = new Class[]{URL.class};

	/**
	 * Default constructor.
	 * @throws ResourceException, in case any error takes place
	 * during the construction of the instance.
	 */
	public ClientClasspathUpdater() throws ResourceException {
		URL [] lUrls = getClasspathUrls(InterfaceExplica.DEFAULT_CLASSPATH);
		
		StringBuffer buf = new StringBuffer();
		for (int i=0; i<lUrls.length; i++) {
			buf.append("\t\t");
			buf.append(lUrls[i].getPath());
			if (i<lUrls.length-1)
				buf.append("\n");
		}
		
		ClientContext.getInstance().getLogger().
			info("List of URLs added to the client classpath: \n" + buf.toString());
	}
	
	/**
	 * Constructor for a provided classpath (as String).
	 * @param classpath String, classpath that must be added to the
	 * Java virtual machine.
	 * @throws ResourceException, in case any error takes place
	 * during the construction of the instance.
	 */
	public ClientClasspathUpdater(String classpath) throws ResourceException {
		URL[] lUrls = getClasspathUrls(classpath);
		
		StringBuffer buf = new StringBuffer();
		for (int i=0; i<lUrls.length; i++) {
			buf.append("\t\t");
			buf.append(lUrls[i].getPath());
			if (i<lUrls.length-1)
				buf.append("\n");
		}
		
		ClientContext.getInstance().getLogger().
			info("List of URLs added to the client classpath: \n" + buf.toString());
	}
	
	/**
	 * Constructor for a provided classpath (as URL list).
	 * @param lUrls URL[], list of urls of the jars that must be
	 * added to the Java virtual machine classpath. 
	 */
	public ClientClasspathUpdater(URL[] lUrls){
		StringBuffer buf = new StringBuffer();
		for (int i=0; i<lUrls.length; i++) {
			buf.append("\t\t");
			buf.append(lUrls[i].getPath());
			if (i<lUrls.length-1)
				buf.append("\n");
		}
		
		ClientContext.getInstance().getLogger().
			info("List of URLs added to the client classpath: \n" + buf.toString());
	}
	
	/**
	 * Build the list of the URLs referred by the classpath. It must include
	 * all the directories, all the files, and all the jars contained
	 * in the classpath.
	 * @param classpath String, classpath of the application. List of paths
	 * separated by the path.separator used by the system.
	 * @return URL[], list of individual URLs contained in the classpath.
	 * @throws ResourceException if any error take place during the building
	 * of the list of URLs enclosed by the classpath.
	 */
	public static final URL[] getClasspathUrls(String classpath) throws ResourceException {
		ArrayList lUrls = new ArrayList();
		
		// Convert classpath to a list of URLS
		if (classpath != null && classpath.length() > 0) {
			try {
				// Get the system's path separator
				String pathSep = System.getProperty(InterfaceExplica.PATH_SEPARATOR_PROPERTYNAME);
				
				if (pathSep == null || pathSep.length() <= 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_NULL_PATHSEPARATOR);
				}

				if (classpath == null || classpath.length() <= 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_NULL_CLASSPATH);
				}
				
				// Get a list with all the individual paths that form the classpath
				String[] lPaths = classpath.split(pathSep);
				if (lPaths == null || lPaths.length == 0) {
					throw new ResourceException(InterfaceErrors.ERR_RESOURCE_MALFORMED_CLASSPATH);
				}
				
				// Iterate each of the paths that form the classpath
				for (int i=0; i<lPaths.length; i++) {
					String path = lPaths[i];
					
					// if the path is a valid file add it to the list of URLs
					if (checkFile(path)) {
						lUrls.add(getPathURL(path));
						 ClientContext.getInstance().getLogger().
						 	debug("Add file to the resources urls: " + path);
					} // if path is a file
					// if the path is a valid directory
					else if (checkDirectory(path)) {
						// Add the directory to the list of URLs
						lUrls.add(getPathURL(removeLastSlash(path)));
						ClientContext.getInstance().getLogger().
					 		debug("Add directory to the resources urls: " + removeLastSlash(path));
						
						// Add all the jars inside the directory to the list of URLs
						URL[] lJarUrls = getDirectoryJarsURL(removeLastSlash(path));
						if (lJarUrls != null && lJarUrls.length > 0)
							lUrls.addAll(Arrays.asList(lJarUrls));
					} // else if path is a directory
				} // for each path in the classpath
			} // try
			catch (ResourceException ex) {
				ClientContext.getInstance().getLogger().
					error("Error in client classpath updating!", ex);
				throw ex;
			} // catch
		} // if classpath not empty

		ClientContext.getInstance().getLogger().
 			info("------------ List of added URLs to the client classpath -------------");
		for (int i=0; i<lUrls.size(); i++) {
			ClientContext.getInstance().getLogger().
	 			info(((URL)lUrls.get(i)).getPath());
		} // for each URL in the classpath
		ClientContext.getInstance().getLogger().
			info("---------------------------------------------------------------------");
		
		return (lUrls != null && !lUrls.isEmpty())?(URL[])lUrls.toArray(new URL[lUrls.size()]):null;
	}
	
	/**
	 * Remove the last slash from the directory name. All the directories
	 * of the classpath must end without a slash.
	 * @param directory String, directory path to add to the classpath.
	 * @return String, correct and formatted directory path
	 * @throws ResourceException, if directory is null or empty. Also if the
	 * system's file separator is null or empty.
	 */
	private static final String removeLastSlash(String directory) throws ResourceException{
		String result = directory;
		String dirSeparator = null;

		// Get the system's file separator, stored in the system's property
		dirSeparator = System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME);

		// If directory is empty or null throw an exception
		if (directory == null || directory.length() <= 0) {
			throw new ResourceException(null, directory, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		// If system's file separator is empty or null throw an exception
		if (dirSeparator == null || dirSeparator.length() <= 0) {
			throw new ResourceException(null, directory, InterfaceErrors.ERR_RESOURCE_NULL_FILESEPARATOR);
		}
		
		// If directory ends with a file separator, remove the last file separator
		if(directory.endsWith(dirSeparator)) {
			result = directory.substring(0,directory.length() - dirSeparator.length());
		}		
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid file (if it's really a single file
	 * and exists).
	 * @param path String, path of the file.
	 * @return boolean, True if the path is a valid file, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkFile(String path) throws ResourceException{
		boolean result = false;
		
		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			File f = null;
			try {
				URL url = ClientClasspathUpdater.class.getResource(getAbsolutePath(path));
				if (url != null) {
					System.out.println("CHECK FILE PATH = " + url.getPath());
					f = new File(url.getPath());
				}
				else {
					f = new File(path);
				}
				result = f.exists() && f.isFile();
			}
			catch (Exception e) {
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, e);
			}
		}
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid directory (if it's really a directory
	 * and exists).
	 * @param path String, path of the directory.
	 * @return boolean, True if the path is a valid directory, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkDirectory(String path) throws ResourceException{
		boolean result = false;
		
		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			File f = null;
			try {
				URL url = ClientClasspathUpdater.class.getResource(getAbsolutePath(path));
				if (url != null) {
					System.out.println("CHECK DIRECTORY PATH = " + url.getPath());
					f = new File(url.getPath());
				}
				else {
					f = new File(path);
				}
				result = f.exists() && f.isDirectory();
			}
			catch (Exception e) {
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, e);
			}
		}
		
		return result;
	}
	
	/**
	 * Checks if the path is a valid JAR file (if it's really a JAR file
	 * and exists).
	 * @param path String, path of the file.
	 * @return boolean, True if the path is a valid JAR file, False in any
	 * other case.
	 * @throws ResourceException when the path is malformed, null or empty.
	 */
	private static final boolean checkJarFile(String path) throws ResourceException{
		boolean result = false;

		if(path == null || path.length()<= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		else {
			ZipFile f = null;
			try {
				URL url = ClientClasspathUpdater.class.getResource(getAbsolutePath(path));
				if (url != null) {
					System.out.println("CHECK JAR PATH = " + url.getPath());
					f = new ZipFile(url.getPath());
				}
				else {
					f = new ZipFile(path);
				}
				result = f.size() > 0 && f.getName().toUpperCase().endsWith(".JAR");
			}
			catch (Exception e) {
				e.printStackTrace();
				result = false;
			}
		}
		
		return result;
	}
	
	/**
	 * Get the URL to the specified path (directory or single file).
	 * @param path String, path of the directory or file.
	 * @return URL, url to the specified path.
	 * @throws ResourceException, if the path is null, empty or malformed.
	 */
	private static final URL getPathURL(String path) throws ResourceException {
		if (path == null || path.length() <= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		try {
			File f = null;
			URL url = ClientClasspathUpdater.class.getResource(getAbsolutePath(path));
			if (url != null) {
				System.out.println("GET URL PATH = " + url.getPath());
				f = new File(url.getPath());
			}
			else {
				f = new File(path);
			}
			return f.toURI().toURL();
		}
		catch (Exception ex) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, ex);
		}
	}
	
	/**
	 * Get the URLs of all the jar files contained in a directory path.
	 * @param path String, path of the directory.
	 * @return URL[], list with the URL of all the jar files contained 
	 * in the path.
	 * @throws ResourceException, if the path is null, empty or malformed.
	 */
	private static final URL[] getDirectoryJarsURL(String path) throws ResourceException {
		ArrayList lUrls = new ArrayList();
		
		if (path == null || path.length() <= 0) {
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
		
		try {
			// Get the system's file separator, stored in the system's property
			String dirSeparator = System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME); 
			
			// Open the directory file
			File f = null;
			URL url = ClientClasspathUpdater.class.getResource(getAbsolutePath(path));
			if (url != null) {
				System.out.println("GET DIRECTORY JARS PATH = " + url.getPath());
				f = new File(url.getPath());
			}
			else {
				f = new File(path);
			}
			
			ClientContext.getInstance().getLogger().
				debug("Directory: " + path + " opened!");
			
			// Check the directory is a valid directory
			if (f.exists() && f.isDirectory()) {
				// Get all the files belonging to the directory
				String[] lFiles = f.list();
				ClientContext.getInstance().getLogger().
					debug("Files in the directory: " + lFiles);
				
				// for each file/subdirectory in the directory
				for (int i=0; lFiles != null && i<lFiles.length; i++) {
					String filepath = path + dirSeparator + lFiles[i];
					ClientContext.getInstance().getLogger().
						debug("Analizing directory's file: " + filepath);
					
					// if the file is a JAR file then add it to the list of URLs
					if (checkJarFile(filepath)) {
						lUrls.add(getPathURL(filepath));
						ClientContext.getInstance().getLogger().
							debug("Add jar to the resources urls: " + filepath);
					}
					// if the file is a directory then navigate the subdirectory to add the jar
					// files that may be stored in the subdirectory and its succesors.
					else if (checkDirectory(filepath)) {
						lUrls.addAll(Arrays.asList(getDirectoryJarsURL(filepath)));
					}
				} // for
			} // if path is a valid directory
			else {
				throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_DIRECTORY);
			} // else path is not a valid directory

			// Log the list of added URLs to the client classpath
			StringBuffer buf = new StringBuffer();
			for (int i=0; i<lUrls.size(); i++) {
				buf.append("\t\t");
				buf.append(((URL)lUrls.get(i)).getPath());
				if (i<lUrls.size()-1)
					buf.append("\n");
			}
			
			ClientContext.getInstance().getLogger().
				info(	"------------ List of added URLs to the client classpath -------------\n" + 
						buf.toString() + 
						"\n---------------------------------------------------------------------");

			return (lUrls != null && !lUrls.isEmpty())?(URL[])lUrls.toArray(new URL[lUrls.size()]):null;
		}
		catch (Exception ex) {
			ex.printStackTrace();
			throw new ResourceException(null, path, InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH, ex);
		}
	}
	
	/**
	 * Add the URL to the System's classloader classpath
	 * @param u URL, URL to add to the classpath.
	 * @throws ResourceException, if any error takes place during
	 * the update of the classpath (url not found, malformed, etc.)
	 */
	public static void addURL(URL u) throws ResourceException {
		URL urls[] = new URL[ ]{ u };
		URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
 
		try{	
			Class sysclass = URLClassLoader.class;
 
			Method method = sysclass.getDeclaredMethod("addURL", parameters);
			method.setAccessible(true);
			method.invoke(sysloader,new Object[]{ u });
			
			ClientContext.getInstance().getLogger().
				debug("URL: " + u.getPath() + " added to the client's Java machine classpath!");
		} catch (Exception ex) {
			ex.printStackTrace();
			throw new ResourceException(InterfaceErrors.ERR_RESOURCE_INCORRECT_PATH);
		}
	}
	
	/**
	 * Add the directories and jar files included in the local classpath,
	 * to the System's classloader classpath.
	 * @param classpath String, local classpath to add.
	 * @throws ResourceException if any error takes place during the update
	 * of the System's classpath.
	 */
	public static void addClasspath(String classpath) throws ResourceException {
		try {
			URL[] lUrls = ClientClasspathUpdater.getClasspathUrls(classpath);
			for (int i=0; i<lUrls.length; i++) {
				addURL(lUrls[i]);
			}
		}
		catch (ResourceException ex) {
			ex.printStackTrace();
		}
	}
	
	public static String getAbsolutePath(String path) {
		if (path != null && path.length() > 0) {
			if (!path.startsWith(System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME)))
				return System.getProperty(InterfaceExplica.FILE_SEPARATOR_PROPERTYNAME) + path;
			else
				return path;
		}
		else
			return null;
	}
	
	/**
	 * Returns tag Id assigned to CVS source file.
	 */
	public static String getRevision() {
		return "$Id: ClientClasspathUpdater.java,v 1.3 2009-09-17 12:10:05 mario Exp $";
	}
}